昨天認識了 Android 常用的 Widget,今天要進一步練習 Android 的點擊事件。透過四個 MainActivity 範例,學習如何使用 View Binding、findViewById()、Lambda、View.OnClickListener 與 when,讓按鈕真正控制畫面上的文字與圖片。參考資料:Android Developers-輸入事件
今天會完成以下功能:
when 判斷被點擊的元件。ImageView 動態切換圖片。ImageButton 建立圖片按鈕選單。View.OnClickListener 的差異。參考資料:Android Developers-Button|Android Developers-View Binding
Android App 顯示畫面後,程式通常會等待使用者操作。例如按下登入按鈕、切換圖片、增加購物車數量或選擇功能。這些操作都屬於輸入事件。參考資料:Android Developers-輸入事件
Android 可以透過 setOnClickListener() 監聽元件的點擊事件:
button.setOnClickListener {
// 使用者按下按鈕後執行的程式
}
當使用者按下 button,大括號裡的程式就會被執行。這種寫法使用 Kotlin Lambda,可以省略完整的匿名類別與 onClick() 函式。參考資料:Android Developers-回應點擊事件
第一個範例使用 View Binding 取得 XML 中的三個按鈕及三個 TextView。按下不同按鈕後,程式會修改對應的文字。參考資料:Android Developers-View Binding
package com.example.layout3
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.layout3.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// 宣告 activity_main.xml 對應的 View Binding 物件。
// lateinit 代表稍後會在 onCreate() 中進行初始化。
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge,讓內容可以延伸至系統列區域。
enableEdgeToEdge()
// 根據 activity_main.xml 建立 ActivityMainBinding。
binding = ActivityMainBinding.inflate(layoutInflater)
// binding.root 代表 XML 最外層的根 View。
setContentView(binding.root)
// 取得狀態列及導覽列的 Insets,
// 再將其設定成根版面的 Padding,避免畫面遭系統列遮住。
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 第一個按鈕被點擊後,修改第一個 TextView。
binding.button1.setOnClickListener {
binding.textViewText1.text = "Button-1 Clicked"
}
// 第二個按鈕被點擊後,修改第二個 TextView。
binding.button2.setOnClickListener {
binding.textViewText2.text = "Button-2 Clicked"
}
// 第三個按鈕被點擊後,修改第三個 TextView。
binding.button3.setOnClickListener {
binding.textViewText3.text = "Button-3 Clicked"
}
}
}
參考資料:Android Developers-在 Activity 使用 View Binding
View Binding 會依照 XML 檔名自動產生 Binding 類別。例如:
activity_main.xml
↓
ActivityMainBinding
XML 元件的 ID 也會轉換成 Binding 屬性:
<Button
android:id="@+id/button1" />
在 Kotlin 中便可以透過以下方式取得:
binding.button1
因此不需要另外撰寫:
val button1 = findViewById<Button>(R.id.button1)
View Binding 可以減少重複程式碼,也能在編譯階段檢查元件 ID 與型別。參考資料:Android Developers-View Binding 與 findViewById
第一個範例使用 Lambda 處理按鈕事件:
binding.button1.setOnClickListener {
binding.textViewText1.text = "Button-1 Clicked"
}
它相當於以下完整匿名物件寫法:
binding.button1.setOnClickListener(
object : android.view.View.OnClickListener {
override fun onClick(view: android.view.View?) {
binding.textViewText1.text = "Button-1 Clicked"
}
}
)
由於 View.OnClickListener 只有一個抽象函式 onClick(),Kotlin 可以利用 SAM 轉換,簡化成 Lambda。參考資料:Kotlin-SAM conversions
第二個範例使用三個按鈕製作計數器:
| 按鈕 ID | 功能 |
|---|---|
button_plus |
數值加一 |
button_minus |
數值減一 |
button_reset |
數值歸零 |
三個按鈕沒有分別建立 Lambda,而是共同使用 MyButton 事件監聽器,再透過 when 判斷實際被點擊的按鈕。參考資料:Android Developers-View.OnClickListener
package com.example.widget_2
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
// 宣告歸零、減一及加一按鈕。
private lateinit var buttonReset: Button
private lateinit var buttonMinus: Button
private lateinit var buttonPlus: Button
// 顯示目前計數結果的 TextView。
private lateinit var textViewCounter: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge 顯示。
enableEdgeToEdge()
// 載入 activity_main.xml。
setContentView(R.layout.activity_main)
// 處理狀態列與導覽列範圍。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 使用 findViewById() 取得 XML 中的元件。
textViewCounter =
findViewById(R.id.txt_CountValue)
buttonPlus =
findViewById(R.id.button_plus)
buttonMinus =
findViewById(R.id.button_minus)
buttonReset =
findViewById(R.id.button_reset)
// 建立一個共用的事件監聽器物件。
val buttonListener = MyButton()
// 三個按鈕共同使用相同的事件監聽器。
buttonPlus.setOnClickListener(buttonListener)
buttonMinus.setOnClickListener(buttonListener)
buttonReset.setOnClickListener(buttonListener)
}
/**
* 三個按鈕共用的點擊事件監聽器。
*
* inner 讓 MyButton 可以存取 MainActivity 的
* textViewCounter 等成員。
*/
inner class MyButton : View.OnClickListener {
override fun onClick(view: View?) {
// view 代表目前被使用者點擊的元件。
// 讀取 view.id 後,使用 when 判斷按鈕功能。
when (view?.id) {
R.id.button_plus -> {
// 讀取 TextView 目前顯示的文字。
val data =
textViewCounter.text.toString()
// 使用 toIntOrNull() 安全轉換成整數。
// 如果轉換失敗,就使用預設值 0。
var number =
data.toIntOrNull() ?: 0
// 數值加一。
number++
// 將整數轉回字串並顯示。
textViewCounter.text =
number.toString()
}
R.id.button_minus -> {
// 讀取並轉換目前數值。
val data =
textViewCounter.text.toString()
var number =
data.toIntOrNull() ?: 0
// 只有大於零時才減一,
// 避免計數器出現負數。
if (number > 0) {
number--
}
// 顯示計算結果。
textViewCounter.text =
number.toString()
}
R.id.button_reset -> {
// 將計數器直接歸零。
val number = 0
textViewCounter.text =
number.toString()
}
}
}
}
}
TextView.text 的型別是 CharSequence,不能直接執行整數加法,因此必須先轉換成 String,再轉成 Int:
val data = textViewCounter.text.toString()
var number = data.toIntOrNull() ?: 0
number++
textViewCounter.text = number.toString()
完整轉換流程如下:
TextView.text
→ CharSequence
→ String
→ Int
→ 執行加減運算
→ String
→ 寫回 TextView.text
使用 toIntOrNull() 比 toInt() 更安全。若文字不是合法整數,toInt() 會產生 NumberFormatException;toIntOrNull() 則會回傳 null,再由 Elvis 運算子 ?: 提供預設值。參考資料:Kotlin-toIntOrNull
Kotlin 的巢狀類別預設不會持有外部類別的參考,因此不能直接存取 MainActivity 的屬性。加上 inner 後,內部類別就能操作外部 Activity 的 textViewCounter。參考資料:Kotlin-Nested and inner classes
inner class MyButton : View.OnClickListener {
override fun onClick(view: View?) {
textViewCounter.text = "0"
}
}
因為 MyButton 會持有 MainActivity 的參考,所以它的生命週期不應超過 Activity,否則可能造成記憶體無法釋放。這個範例的監聽器只由 Activity 畫面元件使用,因此符合一般使用方式。參考資料:Android Developers-記憶體管理概觀
多個按鈕共用同一個監聽器時,可以從 onClick() 的 view 參數取得被點擊元件的 ID:
when (view?.id) {
R.id.button_plus -> {
// 加一
}
R.id.button_minus -> {
// 減一
}
R.id.button_reset -> {
// 歸零
}
}
這種寫法適合功能相近、數量較多的按鈕。所有點擊行為集中在同一處,比較容易統一維護。參考資料:Kotlin-when 運算式
第三個範例在畫面中放置一個 ImageView、三個 Button 和一個 TextView。按下不同按鈕後,程式會切換圖片及說明文字。參考資料:Android Developers-ImageView
圖片必須先放入 Android 專案的 Drawable 資源目錄:
app/src/main/res/drawable/img1.jpg
app/src/main/res/drawable/img2.jpg
app/src/main/res/drawable/img3.jpg
Android 建置專案時,會為圖片產生對應的資源 ID,例如 R.drawable.img1。資源檔名只能使用小寫英文字母、數字及底線。參考資料:Android Developers-Drawable 資源
package com.example.widget_3
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : AppCompatActivity() {
// 顯示目前圖片的說明文字。
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge 顯示。
enableEdgeToEdge()
// 載入 Activity 的 XML 版面。
setContentView(R.layout.activity_main)
// 處理系統狀態列及導覽列範圍。
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.main)
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 取得顯示圖片的 ImageView。
val imageView =
findViewById<ImageView>(R.id.imageView)
// 取得三個圖片切換按鈕。
val button1 =
findViewById<Button>(R.id.button)
val button2 =
findViewById<Button>(R.id.button2)
val button3 =
findViewById<Button>(R.id.button3)
// 取得顯示圖片說明的 TextView。
textView =
findViewById(R.id.textView)
// 按下第一個按鈕,顯示 img1。
button1.setOnClickListener {
imageView.setImageResource(
R.drawable.img1
)
textView.text = "This is flower 1"
}
// 按下第二個按鈕,顯示 img2。
button2.setOnClickListener {
imageView.setImageResource(
R.drawable.img2
)
textView.text = "This is flower 2"
}
// 按下第三個按鈕,顯示 img3。
button3.setOnClickListener {
imageView.setImageResource(
R.drawable.img3
)
textView.text = "This is flower 3"
}
}
}
參考資料:Android Developers-ImageView.setImageResource
setImageResource() 可以將 Drawable 資源設定成 ImageView 顯示的圖片:
imageView.setImageResource(R.drawable.img1)
其中:
imageView:準備更新的圖片元件。setImageResource():設定圖片資源的方法。R.drawable.img1:Drawable 圖片的資源 ID。參考資料:Android Developers-ImageView API
如果需要清除目前圖片,可以傳入 0:
imageView.setImageResource(0)
setImageResource() 適合顯示 App 內建的 Drawable 資源。如果需要載入網路圖片或較大的照片,實務上通常會使用 Coil 或 Glide 等圖片載入函式庫,以便處理背景下載、快取及記憶體管理。參考資料:Android Developers-載入圖片
按下按鈕時,可以在同一個 Lambda 中執行多個動作:
button1.setOnClickListener {
// 切換 ImageView 圖片。
imageView.setImageResource(R.drawable.img1)
// 修改 TextView 說明。
textView.text = "This is flower 1"
}
這種操作可以用於相簿、商品圖片、教學卡片或動植物圖鑑等功能。參考資料:Android Developers-回應點擊事件
第四個範例使用六個 ImageButton。這些圖片按鈕共同使用 MyImgButton 事件監聽器,再根據按鈕 ID 修改三個 TextView。參考資料:Android Developers-ImageButton
| ImageButton | 修改的 TextView | 顯示內容 |
|---|---|---|
imageButton1 |
tv1 |
this is flower 1 |
imageButton2 |
tv2 |
this is flower 2 |
imageButton3 |
tv3 |
this is flower 3 |
imageButton4 |
tv1 |
this is flower 4 |
imageButton5 |
tv2 |
this is flower 5 |
imageButton6 |
tv3 |
this is flower 6 |
參考資料:Android Developers-View.OnClickListener
package com.example.widget_41f
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.widget_41f.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
// 宣告 activity_main.xml 對應的 View Binding。
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 啟用 Edge-to-Edge。
enableEdgeToEdge()
// 建立 ActivityMainBinding。
binding =
ActivityMainBinding.inflate(layoutInflater)
// 將 Binding 的根 View 設定成 Activity 畫面。
setContentView(binding.root)
// 處理狀態列與導覽列範圍。
ViewCompat.setOnApplyWindowInsetsListener(
binding.main
) { view, insets ->
val systemBars =
insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
// 設定第一個 TextView 的初始文字。
binding.tv1.text = "1234"
// 建立一個共用的 ImageButton 事件監聽器。
val imageButtonListener = MyImgButton()
// 六個 ImageButton 共用相同的監聽器物件。
binding.imageButton1.setOnClickListener(
imageButtonListener
)
binding.imageButton2.setOnClickListener(
imageButtonListener
)
binding.imageButton3.setOnClickListener(
imageButtonListener
)
binding.imageButton4.setOnClickListener(
imageButtonListener
)
binding.imageButton5.setOnClickListener(
imageButtonListener
)
binding.imageButton6.setOnClickListener(
imageButtonListener
)
}
/**
* 六個 ImageButton 共用的點擊事件監聽器。
*
* 使用 inner 宣告後,可以直接存取
* MainActivity 中的 binding。
*/
inner class MyImgButton : View.OnClickListener {
override fun onClick(view: View?) {
// 根據被點擊 ImageButton 的 ID,
// 決定要修改哪一個 TextView。
when (view?.id) {
R.id.imageButton1 -> {
binding.tv1.text =
"this is flower 1"
}
R.id.imageButton2 -> {
binding.tv2.text =
"this is flower 2"
}
R.id.imageButton3 -> {
binding.tv3.text =
"this is flower 3"
}
R.id.imageButton4 -> {
binding.tv1.text =
"this is flower 4"
}
R.id.imageButton5 -> {
binding.tv2.text =
"this is flower 5"
}
R.id.imageButton6 -> {
binding.tv3.text =
"this is flower 6"
}
}
}
}
}
參考資料:Android Developers-View Binding|Android Developers-事件監聽器
原始程式為每個按鈕各建立一個 MyImgButton:
binding.imageButton1.setOnClickListener(MyImgButton())
binding.imageButton2.setOnClickListener(MyImgButton())
binding.imageButton3.setOnClickListener(MyImgButton())
binding.imageButton4.setOnClickListener(MyImgButton())
binding.imageButton5.setOnClickListener(MyImgButton())
binding.imageButton6.setOnClickListener(MyImgButton())
這樣可以正常運作,但會建立六個不同的監聽器物件。由於六個按鈕的事件都交給相同類別處理,因此可以只建立一次:
val imageButtonListener = MyImgButton()
binding.imageButton1.setOnClickListener(imageButtonListener)
binding.imageButton2.setOnClickListener(imageButtonListener)
binding.imageButton3.setOnClickListener(imageButtonListener)
binding.imageButton4.setOnClickListener(imageButtonListener)
binding.imageButton5.setOnClickListener(imageButtonListener)
binding.imageButton6.setOnClickListener(imageButtonListener)
共用物件能更明確表達「六個按鈕由同一個監聽器集中處理」的設計。參考資料:Android Developers-setOnClickListener
| 比較項目 | ImageView |
ImageButton |
|---|---|---|
| 主要用途 | 顯示圖片 | 以圖片作為按鈕 |
| 是否預期互動 | 通常只顯示 | 主要用途就是點擊 |
| 常見用途 | 商品圖、照片、封面 | 播放、刪除、上一張 |
| 設定圖片 | android:src |
android:src |
| 點擊事件 | 可自行設定 | 通常都會設定 |
| 無障礙說明 | 建議設定 | 應設定清楚的功能說明 |
雖然 ImageView 也可以設定 setOnClickListener(),但如果元件的主要功能就是讓使用者點擊,使用 ImageButton 能更清楚地表達其用途。參考資料:Android Developers-ImageButton
圖片按鈕沒有可直接閱讀的文字,因此應設定 android:contentDescription,讓 TalkBack 等螢幕閱讀器能說明按鈕功能。參考資料:Android Developers-描述 UI 元素
<ImageButton
android:id="@+id/imageButton1"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/img1"
android:contentDescription="@string/show_flower_1" />
strings.xml:
<resources>
<string name="app_name">Widget Demo</string>
<string name="show_flower_1">顯示第一張花朵圖片</string>
</resources>
不要只把圖片檔名當成 contentDescription。說明文字應描述按鈕執行的功能,例如「顯示第一張花朵圖片」或「切換到下一張圖片」。參考資料:Android Developers-讓 App 更容易使用
| 範例 | 主要元件 | 取得元件方式 | 事件處理方式 |
|---|---|---|---|
| 範例一 | Button、TextView |
View Binding | 每個按鈕分別使用 Lambda |
| 範例二 | Button、TextView |
findViewById() |
共用 MyButton |
| 範例三 | Button、ImageView |
findViewById() |
每個按鈕分別使用 Lambda |
| 範例四 | ImageButton、TextView |
View Binding | 共用 MyImgButton |
如果按鈕數量不多,而且每個按鈕的功能差異很大,可以分別使用 Lambda;如果有多個相同類型的按鈕,則可以共用 View.OnClickListener,再透過 when 依照 ID 分流。參考資料:Android Developers-回應點擊事件
binding.button1.setOnClickListener {
binding.textViewText1.text = "Button-1 Clicked"
}
binding.button2.setOnClickListener {
binding.textViewText2.text = "Button-2 Clicked"
}
優點是簡單直覺,每一個按鈕的功能都寫在自己的區塊中,適合按鈕數量少、功能差異明顯的畫面。參考資料:Kotlin-Lambda expressions
val listener = MyButton()
buttonPlus.setOnClickListener(listener)
buttonMinus.setOnClickListener(listener)
buttonReset.setOnClickListener(listener)
優點是能集中處理同類型按鈕,適合計算機、數字鍵盤、圖片選單或功能面板等畫面。參考資料:Android Developers-View.OnClickListener
| 比較項目 | findViewById() |
View Binding |
|---|---|---|
| 元件取得方式 | 手動輸入 ID | 自動產生屬性 |
| 型別設定 | 需要指定型別 | 自動判斷型別 |
| 程式碼長度 | 較多 | 較精簡 |
| ID 錯誤檢查 | 較有限 | 可在編譯階段檢查 |
| 新專案建議 | 可學習基本原理 | 建議優先使用 |
findViewById() 有助於初學者理解 XML ID 如何連接 Kotlin 元件;實際開發新專案時,View Binding 通常更安全,也可以減少重複程式碼。參考資料:Android Developers-View Binding
目前計數器直接把數值存放在 TextView 中,每次點擊時都要先讀取文字再轉換成整數。更理想的方式是使用一個 Int 變數保存狀態,TextView 只負責顯示。參考資料:Android Developers-UI 狀態
// 使用變數保存真正的計數資料。
private var count = 0
/**
* 將 count 的最新數值顯示在 TextView。
*/
private fun updateCounterText() {
textViewCounter.text = count.toString()
}
按鈕事件可以改成:
when (view?.id) {
R.id.button_plus -> {
// 修改資料狀態。
count++
// 更新畫面。
updateCounterText()
}
R.id.button_minus -> {
// 確保數值不會低於零。
if (count > 0) {
count--
}
// 更新畫面。
updateCounterText()
}
R.id.button_reset -> {
// 重設資料。
count = 0
// 更新畫面。
updateCounterText()
}
}
這種設計把「資料」與「畫面顯示」分開,不必重複執行字串轉數字。日後進一步學習 ViewModel、LiveData 或 StateFlow 時,也會延續相同的 UI 狀態管理觀念。參考資料:Android Developers-UI Layer
Android 裝置方向改變時,系統可能會銷毀並重新建立 Activity。若計數值只存放在 Activity 的一般變數中,重新建立後就會回到初始值。參考資料:Android Developers-Activity 狀態變更
簡單範例可以使用 onSaveInstanceState() 保存數值:
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Activity 重新建立時,讀取先前保存的計數值。
count = savedInstanceState?.getInt("count") ?: 0
}
override fun onSaveInstanceState(outState: Bundle) {
// 在 Activity 可能被重新建立前保存目前數值。
outState.putInt("count", count)
super.onSaveInstanceState(outState)
}
較完整的 Android 架構通常會使用 ViewModel 保存畫面狀態,並搭配 SavedStateHandle 處理程序遭系統回收後的資料還原。參考資料:Android Developers-儲存 UI 狀態
今天透過四份 MainActivity 程式,完成了 Android Widget 與點擊事件的綜合練習:
findViewById() 取得 Button、TextView 與 ImageView。View.OnClickListener 建立共用監聽器。inner class 存取 Activity 的畫面元件。when 判斷被點擊按鈕的 ID。toIntOrNull() 安全轉換數字。setImageResource() 動態切換 Drawable 圖片。ImageButton 建立圖片按鈕。ImageButton 共用相同監聽器。contentDescription。參考資料:Android Developers-使用 Views 建構 UI|Android Developers-輸入事件
今天的範例看似只是修改文字、增加數字和切換圖片,卻已經包含 Android App 最核心的互動流程:取得元件、監聽操作、修改資料,以及更新畫面。參考資料:Android Developers-UI Layer
下一篇有篇符可能繼續介紹 EditText、CheckBox、RadioButton、Switch 與 Toast,讓 App 不只接收點擊,也能取得使用者輸入、處理選項並顯示操作訊息。參考資料:Android Developers-EditText|Android Developers-Toast